#coding=utf-8
"""
LoNote server 伺服器模組

定義 LoNote 的伺服器 API，與 LoNote Client 或瀏覽器互動。
"""

import mimetypes
import datetime as DT
import xml.etree.ElementTree as ET
import json
import uuid as UM
import os
from urllib.parse import urlparse
import webbrowser
import socket
import time
import threading
import logging

# lolib_dir = os.path.dirname(inspect.stack()[0][1])
# extra_path = os.path.join(lolib_dir, 'gunicorn')
# sys.path.insert(0, extra_path)
# extra_path = os.path.join(lolib_dir, 'gunicorn', 'gunicorn')
# sys.path.insert(0, extra_path)
class MW_Auth():
    def __init__(self, app):
        self.app = app

    def __call__(self, environ, start_response):
        #TODO
        return self.app(environ, start_response)


from .bottle.bottle import *

from lonote.civa.putil import chext
from lonote.civa.tz import local_timezone

from .books import Book, MtimeNotMatch, Bookshelf, BookshelfError
from .im import IMNotExist, IMAlreadyExist, IMValueError
import lonote.info as info

# 變數初始化
STATIC_DIR = '../server/static'
TEMPLATE_PATH.append('../server/template')
BOOKS_DIR = '../config/lonote/book'
INDEX_DIR = '../config/lonote/index'
USER_STATIC_DIR = '../config/lonote/static'

bs = None
app = Bottle(catchall = True)


# ====================================
# 輔助程式碼
# ====================================

def recode(string):
    '''重新編解碼字串'''
    return string.encode('latin-1').decode('utf8')

def get_rc_days():
    '''取得rc_longterm_nochanged的小時數，並確保cookie有初始化'''
    try:
        rc_days = float(request.get_cookie('rc_longterm_nochanged'))
    except:
        rc_days = 90
        response.set_cookie('rc_longterm_nochanged',
                str(rc_days), max_age = 60*60*24*365)
    return rc_days

def gen_sidebar(bookuuid = None, current_uuid = None, record_name = None):
    '''產生 sidebar'''
    try:
        eznav_level = int(request.get_cookie('eznav_level'))
    except:
        eznav_level = 2
        response.set_cookie('eznav_level',
                str(eznav_level), max_age = 60*60*24*365)
    try:
        stnav_level = int(request.get_cookie('stnav_level'))
    except:
        stnav_level = 0
        response.set_cookie('stnav_level',
                str(stnav_level), max_age = 60*60*24*365)
    return template('sidebar',
            bookshelf = bs,
            bookuuid = bookuuid,
            current_uuid = current_uuid,
            record_name = record_name,
            info = info,
            eznav_level = eznav_level,
            stnav_level = stnav_level)

def gen_page(bookuuid = None, uuid = None, record_name = None):
    '''產生普通瀏覽器用頁面'''
    try:
        book = bs.findbook(bookuuid)
        book.title(uuid) # 這行只是檢查頁面存在之用
    except IMNotExist:
        raise HTTPError(404)
    sidebar = gen_sidebar(bookuuid, uuid, record_name)
    return template('content_page',
            book = book,
            current_uuid = uuid,
            record_name = record_name,
            sidebar = sidebar)

def gen_html(bookuuid = None, uuid = None, record_name = None):
    '''產生普通瀏覽器用頁面'''
    try:
        book = bs.findbook(bookuuid)
        book.title(uuid) # 這行只是檢查頁面存在之用
        return template('html',
                book = book,
                current_uuid = uuid,
                record_name = record_name)
    except IMNotExist:
        raise HTTPError(404)

# ====================================
# 伺服器工作
# ====================================

# 靜態檔案

@app.route('/static/<filename:path>')
def static(filename):
    '''讀取全站共用的靜態檔案'''
    rt = static_file(filename, root = USER_STATIC_DIR)
    if type(rt) == HTTPError:
        return static_file(filename, root = STATIC_DIR)
    else:
        return rt

@app.route('/favicon.ico')
def favicon():
    '''回送favicon'''
    redirect('/static/icon/favicon.ico')

# Book 頁面

@app.route('/site/<bookuuid>')
@app.route('/site/<bookuuid>/')
def book(bookuuid):
    '''書籍頁面，回傳一個可以直接導覽整本書的畫面'''
    redirect('/site/{bu}/{bu}/'.format(bu = bookuuid))

# Sidebar 頁面
@app.route('/sidebar')
@app.route('/sidebar/')
@app.route('/sidebar/<bookuuid>/<uuid>/')
def sidebar(bookuuid = None, uuid = None):
    '''產生sidebar的html訊息'''
    record_name = request.query.record_name
    return gen_sidebar(bookuuid, uuid, record_name)

# index 頁面
@app.route('/index')
def index():
    '''產生獨立的 index 頁面'''
    return template('index', ebooks = bs.ebooks())

# Content 頁面

@app.route('/site/<bookuuid>/<uuid>/')
def content(bookuuid, uuid):
    '''個別筆記頁'''
    try:
        book = bs.findbook(bookuuid)
        original_book_title = book.title(uuid)
    except (BookshelfError, IMNotExist):
        # 檢查筆記有沒有存在別本書中，有則跳出重定向頁
        redirect_data = []
        for book in bs.books.values():
            try:
                title = book.title(uuid)
                newpath = '/site/{buuid}/{uuid}/'.format(
                        buuid = book.book_uuid(),
                        uuid = uuid)
                if request.query_string:
                    newpath += '?' + request.query_string
                redirect_data.append({
                    'title': title,
                    'url': newpath,
                    'booktitle': book.title()})
            except IMNotExist: continue
        if len(redirect_data) >= 2:
            return template('redirect',
                    redirect_data = redirect_data)
        elif len(redirect_data) == 1:
            redirect(newpath) # 單一頁面不用重定向直接跳轉
        raise HTTPError(404) # 如果無論如何找不到，則只能放棄

    record_name = request.query.record_name
    # 要求頁面

    # 選擇用哪種方式給出頁面
    try:
        if request.query.mode == 'raw':
            if record_name: # 如果要求歷史記錄
                content = book.record(uuid, name = record_name)
            else:
                content = book.content(uuid)
            return content
        elif request.query.mode == 'html':
            return gen_html(bookuuid, uuid, record_name)
        else:
            return gen_page(bookuuid, uuid, record_name)
    except IMNotExist:
        raise HTTPError(404)

@app.put('/site/<bookuuid>/<uuid>/')
def content_put(bookuuid, uuid):
    '''更新內容'''
    def content_update():
        book = bs.findbook(bookuuid)
        new_content = request.forms.content
        # 之前網頁記錄的 mtime 在此變成 basetime
        basetime = DT.datetime.strptime(
                request.forms.modified_time, book.time_format)
        # 檢查是否要記錄一個舊版本
        now = DT.datetime.now(local_timezone())
        delta = now - basetime
        rc_days = get_rc_days()
        if rc_days > 0:
            rc_delta = DT.timedelta(days = rc_days)
            if delta > rc_delta:
                hist_xml = book.history_xml(uuid)
                e_hist = ET.fromstring(hist_xml)
                match = './record/frozen_time'
                recent_records = [True for e in e_hist.findall(match) if (now - DT.datetime.strptime(e.text, book.time_format)) <= delta]
                # 如果之前這個版本已有被記錄就不用再記一次
                if len(recent_records) == 0:
                    book.record(uuid, comment = 'AUTO: Due long term ({} days) no be changed'.format(delta.days))
        # content寫入
        try:
            new_mtime = book.content(uuid, new_content, basetime = basetime)
            # 把更新後的訊息與時間回流回去。
            j = json.dumps({
                'mtime_stamp': new_mtime.strftime(book.time_format),
                'time_format': book.time_format,
                'msg': 'OK ✔'})
            return j
        except MtimeNotMatch:
            info = 'edit conflicts ✘'
            raise HTTPResponse(body = info, status = 403)

    def title_update():
        book = bs.findbook(bookuuid)
        return book.title(uuid = uuid, title = request.forms.new_title)

    def position_update():
        '''更新當前節點位置'''
        # 分析使用者輸入的原始資料
        def get_target_parent_buuid_uuid(purl):
            '''取得新位置的(bookuuid, parent uuid)'''
            try:
                if purl == '':
                    book = bs.findbook(bookuuid)
                    puuid, index = book.position(uuid)
                    return (bookuuid, puuid)
                try: # 先試試是否直接就是uuid
                    puuid = str(UM.UUID(purl))
                    # 如果上行失敗會拋出錯誤
                    return (bookuuid, puuid)
                except ValueError:
                    # 若為網址，取 split('/') /site/bookuuid/uuid 的第三、四節
                    buuid = str(UM.UUID(urlparse(purl).path.split('/')[2]))
                    puuid = str(UM.UUID(urlparse(purl).path.split('/')[3]))
                    return (buuid, puuid)
            except:
                info = 'parent_url "{}" can not be deal.'.format(
                        purl)
                raise HTTPResponse(body = info, status = 403)

        def get_index(input_index, buuid):
            '''分析使用者輸入的index字串，轉為book能接受的index

                輸入的input_index是以1為起點，book能接受的index是以0為起點
                '''
            try:
                book = bs.findbook(buuid)
                old_parent_uuid, old_index = book.position(uuid)
                if input_index == '':
                    new_index = None
                elif input_index.startswith( ('+', '-')):
                    new_index = old_index + int(input_index)
                else:
                    new_index = int(input_index) - 1 # -1是必要的
                return new_index
            except:
                info = 'index "{}" can not be deal.'.format(
                        input_index)
                raise HTTPResponse(body = info, status = 403)

        # 檢查

        # 指令解碼
        new_buuid, new_puuid = get_target_parent_buuid_uuid(
                request.forms.parent_url)

        # 資料取妥，開始處理
        book = bs.findbook(bookuuid)
        if new_buuid == bookuuid: # 同書藉內部移動的狀況
            new_index = get_index(request.forms.index, new_buuid)
            try:
                book.position_update(uuid, new_puuid, new_index)
            except IMValueError as e:
                raise HTTPResponse(body = str(e), status = 403)
        else: # 跨書藉移動
            new_book = bs.findbook(new_buuid)
            with tempfile.TemporaryDirectory() as wdir:
                # 先匯出
                tb_path = os.path.join(wdir, 'tmpbook')
                book.book_export(uuid, tb_path) # 匯出！
                # 再匯入
                uuid_map = new_book.book_import(new_puuid, tb_path)
            # 再移動
            new_uuid = uuid_map.get(uuid)
            if not new_uuid:
                new_uuid = uuid
            new_index = get_index(request.forms.index, new_buuid)
            new_book.position_update(new_uuid, new_puuid, new_index)
            # 再刪除
            if bookuuid != uuid:
                book.note_delete(uuid, use_trashcan = False)
            else: # 如果整本書都移走，則整本書都刪掉
                bs.removebook(book.fpath)

            # info = 'Move form one to another book are not supported yet!'
            # raise HTTPResponse(body = info, status = 403)

    if request.forms.cmd == 'content_update':
        return content_update()
    elif request.forms.cmd == 'title_update':
        return title_update()
    elif request.forms.cmd == 'position_update':
        return position_update()

@app.post('/site/<bookuuid>/<uuid>/')
def content_post(bookuuid, uuid):
    '''貼上新東西'''
    def make_record(bookuuid, uuid):
        '''更新內容'''
        book = bs.findbook(bookuuid)
        content = request.forms.content
        comment = request.forms.comment
        try:
            name = book.record(uuid, comment = comment, content = content)
            return name
        except IMAlreadyExist:
            raise HTTPResponse(status = 403)

    def note_create(bookuuid, uuid):
        '''新建筆記'''
        book = bs.findbook(bookuuid)
        if request.forms.position == 'sub':
            newuuid = book.note_create(title = request.forms.new_title,
                    parent_uuid = uuid)
        else:
            if bookuuid == uuid:
                # root只有在新建子節點的情況下才能允許sub以外的操作
                info = 'You can not create a sister note of root note.'
                raise HTTPResponse(body = info, status = 403)
            # 計算索引位置，並新建
            parent_uuid, index = book.position(uuid)
            if request.forms.position == 'before':
                newuuid = book.note_create(title = request.forms.new_title,
                        parent_uuid = parent_uuid,
                        order = index)
            elif request.forms.position == 'next':
                newuuid = book.note_create(title = request.forms.new_title,
                        parent_uuid = parent_uuid,
                        order = index + 1)
        # content = '<p>{}</p>'.format(request.forms.new_title)
        content = '<br>'
        book.content(newuuid, content) # 初始化內容
        return '/site/{}/{}/'.format(bookuuid, newuuid)

    def note_export(bookuuid, uuid, fname):
        book = bs.findbook(bookuuid)
        fpath = os.path.join(bs.books_dir, fname)
        try:
            book.book_export(uuid, fpath)
        except IMAlreadyExist as e:
            info = 'Filename: "{}" Already Exist'.format(fname)
            raise HTTPError(403, info)

    def book_import(bookuuid, uuid, fname):
        book = bs.findbook(bookuuid)
        fpath = os.path.join(bs.books_dir, fname)
        try:
            book.book_import(uuid, fpath)
        except IMNotExist as e:
            info = 'Filename: "{}" Not Exist'.format(fname)
            raise HTTPError(403, info)

    if request.forms.cmd == 'make_record':
        return make_record(bookuuid, uuid)
    elif request.forms.cmd == 'note_create':
        return note_create(bookuuid, uuid)
    elif request.forms.cmd == 'note_export':
        fname = request.forms.fname
        return note_export(bookuuid, uuid, fname)
    elif request.forms.cmd == 'book_import':
        fname = request.forms.fname
        return book_import(bookuuid, uuid, fname)
    else:
        raise HTTPError(403, 'Command not found.')

@app.delete('/site/<bookuuid>/<uuid>/')
def note_delete(bookuuid, uuid):
    '''更新內容'''
    book = bs.findbook(bookuuid)
    cmd = request.forms.cmd
    # 刪掉歷史記錄
    try:
        if cmd == 'record_delete':
            record_name = request.forms.record_name
            book.record_delete(uuid, name = record_name)
        elif cmd == 'note_delete':
            puuid = book.note_delete(uuid)
            return '/site/{bu}/{pu}/'.format(bu = bookuuid, pu = puuid)
    except IMNotExist:
        raise HTTPResponse(body = 'Already deleted. You Can\'t do again.',
                status = 403)
    except IMValueError:
        raise HTTPResponse(body = 'You can not delete this page!',
                status = 403)

@app.route('/site/<bookuuid>/<uuid>')
def content_redirect(bookuuid, uuid):
    '''個別筆記頁重定向'''
    tail = ''
    if request.query_string:
        tail += '?' + request.query_string
    new_path = request.path + '/' + tail
    redirect(new_path)

# 資源頁面

@app.route('/site/<bookuuid>/<uuid>/<res_name>')
def resource(bookuuid, uuid, res_name):
    '''讀取附件與內嵌資源'''
    book = bs.findbook(bookuuid)
    res_name = recode(res_name)
    response.content_type = mimetypes.guess_type(res_name)[0]
    try:
        return book.resource(uuid, res_name)
    except IMNotExist:
        raise HTTPError(404)

@app.put('/site/<bookuuid>/<uuid>/<res_name>')
def resource_update(bookuuid, uuid, res_name):
    '''更新附件與內嵌資源'''
    book = bs.findbook(bookuuid)
    res_name = recode(res_name)
    res_type = request.query.res_type
    return book.resource(uuid,
            res_name, res_type, request.body.read())

@app.delete('/site/<bookuuid>/<uuid>/<res_name>')
def resource_delete(bookuuid, uuid, res_name):
    '''刪除資源'''
    book = bs.findbook(bookuuid)
    res_name = recode(res_name)
    return book.resource_delete(uuid, res_name)

# extra頁面

@app.route('/extra/<bookuuid>/<extra_key>')
def extra(bookuuid, extra_key):
    '''透過 extra key 取得 extra 內容'''
    book = bs.findbook(bookuuid)
    obj = book.extra(extra_key)
    if obj is None:
        raise HTTPError(404)
    # 由程式判斷到底是哪一種物件
    mime = mimetypes.guess_type(extra_key)[0]
    if mime is not None:
        response.content_type = mime
    # 不管有沒有判斷出來，最後都應該將讀出的物件回傳
    return str(obj)

@app.post('/extra/<bookuuid>/<extra_key>')
def extra_update(bookuuid, extra_key):
    if request.query.uploadmode == 'form':
        data = request.forms.data
        book = bs.findbook(bookuuid)
        book.extra(extra_key, data)
    redirect('/config/book/{}'.format(bookuuid))

# 其他頁面

@app.route('/search')
@app.route('/search/')
def search():
    '''搜尋資料庫中所有資料'''
    key = request.query.key
    ans = bs.search(key)
    return template('search', results = ans)
    # return 'OK'

@app.route('/start')
@app.route('/start/')
def start_page():
    '''封面'''
    return template('start')

@app.route('/')
@app.route('/last')
@app.route('/last/')
def redirect_to_lastpage():
    '''自動重定向到上次被讀取的頁面'''
    return '<html><head><title>Redirect to last page...</title><script>if(localStorage["last"]){location.pathname = localStorage["last"];}else{location.pathname = "../start";}</script></head><body>Page loading...</body></html>'

# 伺服器操作

@app.route('/config')
def config_page():
    '''設定頁面'''
    get_rc_days() # 用來設定cookie
    return template('config_page', bookshelf = bs, sidebar = gen_sidebar())

@app.route('/config/book/<bookuuid>')
def config_book_page(bookuuid):
    '''單一本書的管理頁'''
    book = bs.findbook(bookuuid)
    return template('config_book_page', book = book)

@app.post('/config/book/<bookuuid>')
def config_book_cmd(bookuuid):
    '''對單一一本書施加特別指令'''
    book = bs.findbook(bookuuid)
    if request.query.cmd == 'reindex':
        book.index_recreate()
        logging.info('Book RE-INDEX: {}'.format(book.title()))
        return '<script>location.href = "/config/book/{}"</script>'.format(bookuuid)
    elif request.query.cmd == 'kill':
        bs.removebook(request.forms.fname)
        logging.info('Book DELETE: {}'.format(request.forms.fname))
        return '<script>location.href = "/config"</script>'

@app.post('/config/book/<bookuuid>/<fname>')
def config_book_flush_download(bookuuid, fname):
    '''對單一書籍發出 flush 指令後執行下載動作'''
    book = bs.findbook(bookuuid)
    response.content_type = 'application/lobook'
    if request.query.cmd == 'flush':
        logging.info('Download Notebook (with flush): {}'.format(book.title()))
        book.flush()
        with open(book.fpath, mode = "rb") as f:
            return f.read()
    elif request.query.cmd == 'get':
        logging.info('Download Notebook (without flush): {}'.format(book.title()))
        with open(book.fpath, mode = "rb") as f:
            return f.read()
    elif request.query.cmd == 'get_backup':
        logging.info('Download Notebook (backup version): {}'.format(book.title()))
        backup_fpath = chext(book.fpath, 'bak');
        with open(backup_fpath, mode = "rb") as f:
            return f.read()

@app.post('/config')
def config_cmd():
    '''操作管理功能……'''
    if request.query.cmd == 'shutdown': # 關閉伺服器
        logging.info('Server shutdown by browser.')
        os._exit(0)

    elif request.query.cmd == 'reload': # 載入選擇的書籍
        loadlist = [recode(fname)
                for fname in request.forms.getlist('fname')]
        bs.lf_init(False, *loadlist)
        bs.loadbooks()
        logging.info('Books RELOAD: "{}"'.format(loadlist))

    elif request.query.cmd == 'createbook': # 加入一本新書
        if not request.forms.fname:
            HTTPError(status = 403, body = 'Must input some thing')
        try:
            newbook = book_create(request.forms.fname)
            logging.info('New Book CREATE: {}'.format(newbook.title()))
            # 導向到新書的首頁
            buuid = newbook.book_uuid()
            return '<script>location.href = "/site/{buuid}/{buuid}/"</script>'.format(buuid = buuid)
        except BookshelfError as e:
            logging.warning('New Book CREATE Failed: ' + str(e))

    elif request.query.cmd == 'uploadbook': # 上傳一本新書
        fname = recode(request.files.uploadfile.filename)
        try:
            book = bs.addbook(fname, request.files.uploadfile.file)
            logging.info('Book UPLOAD Successed: "{}"'.format(fname))
        except BookshelfError:
            logging.warning('Book UPLOAD Failed: "{}" not be accepted!'.format(fname))
            return '<p>This notebook be rejected!</p><p>May be filename collision, or some format problem.'#' <script>location.href = "/config"</script>'
        return '<script>location.href = "/site/{buuid}/{buuid}/"</script>'.format(buuid = book.book_uuid())


    # 導向回原本頁面
    return '<script>location.href = "/config"</script>'

def book_create(fname):
    '''用fname建立新筆記簿'''
    if fname.endswith('.lobook'):
        fname_ext = fname
    else:
        fname_ext = fname + '.lobook'
    # 創建與更名
    b = bs.create_book(fname_ext)
    if request.forms.fname:
        b.title(None, request.forms.fname)
    hello = '''<p>This is your new notebook "{}"</p>
    <p>If you want to create more page, Check your sidebar or press Alt + N.</p>'''.format(b.title(None))
    b.content(None, hello)
    return b

# 錯誤處理

@app.error(404)
def error404(error):
    logging.warning('File Not Found: "{}"'.format(request.path))
    return 'Nothing here, sorry<script>setTimeout(function(){location.pathname = "start"}, 1500);</script>'

# ====================================
# 啟動伺服器
# ====================================

def start_server(books_dir,
        index_dir,
        template_dir,
        user_static_dir,
        static_dir,
        logging_file,
        local = True, port = 10204, debug = False, browser_list = []):
    '''啟動伺服器'''
    global STATIC_DIR
    STATIC_DIR = static_dir
    global USER_STATIC_DIR
    USER_STATIC_DIR = user_static_dir

    # config logging system
    logging.basicConfig(filename = logging_file,
            datefmt = '%Y-%m-%d %H:%M:%S',
            format = '%(levelname)s (%(asctime)s): %(message)s',
            level = 'NOTSET')

    TEMPLATE_PATH.append(template_dir)
    Book.INDEX_DIR = index_dir

    global bs
    bs = Bookshelf(books_dir)

    if local:
        host = '127.0.0.1'
    else:
        host = '0.0.0.0'
    if debug == True:
        quiet = False
    else:
        quiet = True

    # 等待 app.run 執行後啟動 browser
    threading.Thread(
            target = open_browser,
            kwargs = {
                'browser_list': browser_list,
                'host': host,
                'port': port}
            ).start()

    auth_app = MW_Auth(app)

    print('Server starting...')
    logging.info('Server starting...')
    testloop = 0
    while True:
        try:
            run(app = auth_app,host = host, port = port,
                    debug = debug, quiet = quiet)
            break
        except socket.error as e:
            print(e)
            print('retry...')
            if testloop < 10:
                testloop += 1
            else:
                print('Server already exist!')
                logging.warning('Server already exist, cancel!')
                break

def open_browser(browser_list, host, port):
    '''打開 browser'''
    loop = 0
    while True:
        time.sleep(0.1)
        try: # 偵測 socket 有無被用，有則打開瀏覽器
            s = socket.socket()
            s.bind((host, port))
            s.close()
            continue
        except socket.error:
            # browser_list 
            if browser_list is not None:
                open_target = 'http://localhost:{}'.format(port)
                if len(browser_list) == 0:
                    webbrowser.open(open_target)
                else:
                    for browser in browser_list:
                        try:
                            wb = webbrowser.get(browser)
                            wb.open(open_target)
                        except webbrowser.Error as e:
                            print(e)
            break
        if loop > 200:
            break
        else:
            loop += 1

